﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Borg;
using Borg.AddOn;
using Borg.Properties;

public sealed class CompositionEngine : RuntimeElement
{
    private bool _autoComposition;
    private bool _autoOpen;
    private bool _autoPlug;
    private bool _autoTag;

    private ICompositionPermissionHook _compositionPermissionHook;
    private ICompositionEngineApiHook _apiHook;
    private ICompositionEngineNotificationHook _notificationHook;

    public bool AutoComposition
    {
        get { return _autoComposition; }
        set
        {
            Composer.SetProperty(
                this,
                PublicSetAutoComposition,
                value,
                Messages.CompositionEngine_ErrorOnSettingComposerEngineProperty,
                "AutoComposition");
        }
    }

    public bool AutoOpen
    {
        get { return _autoOpen; }
        set
        {
            Composer.SetProperty(
                this,
                PublicSetAutoOpen,
                value,
                Messages.CompositionEngine_ErrorOnSettingComposerEngineProperty,
                "AutoOpen");
        }
    }

    public bool AutoPlug
    {
        get { return _autoPlug; }
        set
        {
            Composer.SetProperty(
                this,
                PublicSetAutoPlug,
                value,
                Messages.CompositionEngine_ErrorOnSettingComposerEngineProperty,
                "AutoPlug");
        }
    }

    public bool AutoTag
    {
        get { return _autoTag; }
        set
        {
            Composer.SetProperty(
                this,
                PublicSetAutoTag,
                value,
                Messages.CompositionEngine_ErrorOnSettingComposerEngineProperty,
                "AutoTag");
        }
    }

    internal CompositionEngine(Runtime runtime)
        : base(runtime)
    {
        _autoComposition = _autoOpen = _autoPlug = _autoTag = true;
    }

    internal void Init(ICompositionPermissionHook compositionPermissionHook,
        ICompositionEngineApiHook apiHook, ICompositionEngineNotificationHook notificationHook)
    {
        _compositionPermissionHook = compositionPermissionHook;
        _apiHook = apiHook;
        _notificationHook = notificationHook;
    }

    private void PublicSetAutoComposition(bool value)
    {
        _apiHook.OnSetProperty(
            value
                ? CompositionEngineOperation.EnableAutoComposition
                : CompositionEngineOperation.DisableAutoComposition);
        
        _autoComposition = value;
    }

    private void PublicSetAutoOpen(bool value)
    {
        _apiHook.OnSetProperty(
            value
                ? CompositionEngineOperation.EnableAutoOpen
                : CompositionEngineOperation.DisableAutoOpen);
        
        _autoOpen = value;
    }

    private void PublicSetAutoPlug(bool value)
    {
        _apiHook.OnSetProperty(
            value
                ? CompositionEngineOperation.EnableAutoPlug
                : CompositionEngineOperation.DisableAutoPlug);
        
        _autoPlug = value;
    }

    private void PublicSetAutoTag(bool value)
    {
        _apiHook.OnSetProperty(
            value
                ? CompositionEngineOperation.EnableAutoTag
                : CompositionEngineOperation.DisableAutoTag);
        
        _autoTag = value;
    }

    internal void InternalAutoOpenSlots(Extension extension)
    {
        foreach (var slot in extension.Slots)
            InternalAutoOpen(slot);
    }

    private void InternalAutoOpen(Slot slot)
    {
        if (!Composer.AllowComposition ||
            !AutoComposition ||
            (!AutoOpen ||
            InstanceStore.InternalIsOpen(slot)) ||
            !InstanceStore.InternalGetAutoOpen(slot) ||
            !Qualifier.InternalWeakQualify(slot.SlotType))
            return;

        LogArgs log;

        if (!Notifier.CanOpen(new SlotEventArgs(slot), out log))
        {
            if (log != null)
                Logger.Log(
                    new LogArgs(
                        log.Type,
                        this,
                        string.Format(
                            Messages.Composer_OpeningSlotCanceled,
                            slot.Id,
                            slot.Name,
                            log.Message),
                        new object[0]));
        }
        else
            Composer.InternalOpen(slot);
    }

    internal void InternalAutoTagAndPlug(Slot slot)
    {
        IDictionary<PlugType, Plug> createdPlugs = new Dictionary<PlugType, Plug>();
        
        InternalAutoConnect(slot, createdPlugs, InternalAutoTag);
        InternalAutoConnect(slot, createdPlugs, InternalAutoPlug);
    }

    internal void InternalAutoTagAndPlug(PlugType plugType)
    {
        foreach (var slot in InstanceStore.InternalGetSlots(plugType.Name))
        {
            IDictionary<PlugType, Plug> createdPlugs = new Dictionary<PlugType, Plug>();

            InternalAutoConnect(slot, plugType, createdPlugs, InternalAutoTag);
            InternalAutoConnect(slot, plugType, createdPlugs, InternalAutoPlug);
        }
    }

    private void InternalAutoConnect(Slot slot, IDictionary<PlugType, Plug> createdPlugs,
        Func<Slot, PlugType, Plug, Plug> connect)
    {
        foreach (var plugType in TypeStore.GetPlugTypes(slot.Name))
            InternalAutoConnect(slot, plugType, createdPlugs, connect);
    }

    private void InternalAutoConnect(Slot slot, PlugType plugType,
        IDictionary<PlugType, Plug> createdPlugs, Func<Slot, PlugType, Plug, Plug> connect)
    {
        var plug1 =
            InstanceStore.InternalGetShared(slot)
                ? InternalGetSharedPlug(slot, plugType)
                : (createdPlugs.ContainsKey(plugType)
                       ? createdPlugs[plugType]
                       : null);
        
        var plug2 = connect(slot, plugType, plug1);
        
        if (plug2 == null || plug2 == plug1)
            return;

        createdPlugs.Add(plugType, plug2);
    }

    private Plug InternalAutoTag(Slot slot, PlugType plugType, Plug plug)
    {
        if (InstanceStore.InternalIsOpen(slot) && Composer.AllowComposition && AutoComposition && AutoTag)
        {
            plug = InternalAutoTag(slot, plugType, plug, InstanceStore.GetAutoTag(slot));

            plug =
                InternalAutoTag(slot, plugType, plug,
                                plug != null
                                    ? InstanceStore.GetAutoTag(plug)
                                    : plugType.AutoTag);
        }

        return plug;
    }

    private Plug InternalAutoTag(Slot slot, PlugType plugType, Plug plug, IEnumerable<string> tags)
    {
        if (tags == null || !Qualifier.InternalWeakQualify(plugType))
            return null;

        var tagDefinitions = slot.SlotType.SlotDefinition.TagDefinitions;
        
        foreach (var tag in tags.Where(tag => !InternalIsTagged(slot, plugType, tag)))
        {
            if (!tagDefinitions.Contains(tag))
                Logger.Log(
                    new LogArgs(
                        LogType.Error,
                        this,
                        string.Format(
                            Messages.Composer_ErrorOnAutoTaggingExtension,
                            (object) slot.Extension.Id,
                            (object) slot.Extension.Name,
                            plug != null
                                ? (object) plug.Extension.Id.ToString(CultureInfo.InvariantCulture)
                                : (object) "-",
                            (object) plugType.ExtensionType.Name,
                            (object) slot.Name,
                            (object) tag,
                            (object) Messages.Composer_Error_TagDefinitionNotFound),
                        new object[0]));
                
            else if (Qualifier.InternalWeakQualify(tagDefinitions[tag]))
            {
                if (plug == null)
                {
                    var myTag = tag;

                    plug = InternalAutoCreatePlug(plugType, InstanceStore.GetShared(slot),
                        logArgs => LogErrorOnAutoTagging(slot, plugType, myTag, logArgs));
                    
                    if (plug == null)
                        continue;
                }

                _notificationHook.OnTryAutoTagging(slot, plug, tag);
                LogArgs log;

                if (!Notifier.CanTag(new TagEventArgs(slot, plug, tag), out log))
                {
                    _notificationHook.OnAutoTaggingAborted(slot, plug, tag);

                    Logger.Log(
                        new LogArgs(
                            (LogType) (log != null ? (int) log.Type : 2),
                            this,
                            string.Format(
                                Messages.Composer_ErrorOnAutoTaggingExtension,
                                (object) slot.Extension.Id,
                                (object) slot.Extension.Name,
                                (object) plug.Extension.Id,
                                (object) plug.Extension.Name,
                                (object) slot.Name,
                                (object) tag,
                                log != null
                                    ? (object) log.Message
                                    : (object) Messages.Composer_Error_CanTagHandlerReturnedFalse),
                            new object[0]));
                }
                else
                {
                    _notificationHook.OnAutoTaggingStarted(slot, plug, tag);
                    Composer.InternalTag(slot, plug, tag);
                }
            }
        }

        return plug;
    }

    private Plug InternalAutoPlug(Slot slot, PlugType plugType, Plug plug)
    {
        if (Composer.AllowComposition &&
            AutoComposition &&
            (AutoPlug && InstanceStore.InternalIsOpen(slot)) &&
            InstanceStore.InternalGetAutoPlug(slot))
        {
            if (plug == null &&
                plugType.AutoPlug &&
                !InternalIsPlugged(slot, plugType) &&
                Qualifier.InternalWeakQualify(plugType))
                plug = InternalAutoCreatePlug(plugType, InstanceStore.GetShared(slot),
                    logArgs => LogErrorOnAutoPlugging(slot, plugType, logArgs));
            
            if (plug != null && InstanceStore.GetAutoPlug(plug))
            {
                _notificationHook.OnTryAutoPlugging(slot, plug);
                LogArgs log;

                if (!Notifier.CanPlug(new CompositionEventArgs(slot, plug), out log))
                {
                    _notificationHook.OnAutoPluggingAborted(slot, plug);

                    Logger.Log(
                        new LogArgs(
                            (LogType) (log != null ? (int) log.Type : 0),
                            this,
                            string.Format(
                                Messages.Composer_ErrorOnAutoPluggingExtension,
                                (object) plug.Extension.Id,
                                (object) plug.Extension.Name,
                                (object) slot.Extension.Id,
                                (object) slot.Extension.Name,
                                (object) slot.Name,
                                log != null
                                    ? (object) log.Message
                                    : (object) Messages.Composer_Error_CanPlugHandlerReturnedFalse),
                            new object[0]));
                }
                else
                {
                    _notificationHook.OnAutoPluggingStarted(slot, plug);
                    Composer.InternalPlug(slot, plug);
                }
            }
        }

        return plug;
    }

    private Plug InternalGetSharedPlug(Slot slot, PlugType plugType)
    {
        var sharedExtension = InstanceStore.InternalGetSharedExtension(plugType.ExtensionType);
        
        return
            sharedExtension != null
            ? sharedExtension.Plugs[slot.Name]
            : null;
    }

    private bool InternalIsTagged(Slot slot, PlugType plugType, string tagName)
    {
        return InternalIsConnected(slot, plugType,
                                   s => InstanceStore.InternalGetTags(s).Where(
                                       t => t.Name.Equals(tagName)).Select(t => t.Plug));
    }

    private bool InternalIsPlugged(Slot slot, PlugType plugType)
    {
        return InternalIsConnected(slot, plugType,
            s => (IEnumerable<Plug>)InstanceStore.GetPluggedPlugs(s));
    }

    private static bool InternalIsConnected(Slot slot, PlugType plugType, Func<Slot, IEnumerable<Plug>> getConnectedPlugs)
    {
        foreach (var plug in getConnectedPlugs(slot))
        {
            if (plug.PlugType == plugType)
                return true;

            if (!plug.PlugType.ExtensionType.IsTransparent) continue;

            if (plug.Extension.Slots.Any(slott => InternalIsConnected(slott, plugType, getConnectedPlugs)))
                return true;
        }

        return false;
    }

    private Plug InternalAutoCreatePlug(PlugType plugType, bool shared, Action<LogArgs> logError)
    {
        var extensionType = plugType.ExtensionType;
        LogArgs log;
        Plug plug;

        if (_compositionPermissionHook.CanCreate(extensionType, out log))
        {
            var extension =
                shared
                    ? Composer.InternalCreateShared(extensionType)
                    : Composer.InternalCreate(extensionType);

            GC.InternalScheduleInspectionOf(new[] {extension});

            plug = extension.Plugs[plugType.Name];
        }
        else
        {
            logError(
                log ?? new LogArgs(
                           LogType.Info,
                           this,
                           Messages.Composer_Error_CanHandlerReturnedFalse,
                           new object[0]));
            
            plug = null;
        }

        return plug;
    }

    private void LogErrorOnAutoTagging(Slot slot, PlugType plugType, string tag, LogArgs log)
    {
        Logger.Log(
            new LogArgs(
                LogType.Info,
                this,
                string.Format(
                    Messages.Composer_ErrorOnAutoTaggingExtension,
                    (object) slot.Extension.Id,
                    (object) slot.Extension.Name,
                    (object) -1,
                    (object) plugType.ExtensionType.Name,
                    (object) plugType.Name,
                    (object) tag,
                    (object) log.Message),
                new object[0]));
    }

    private void LogErrorOnAutoPlugging(Slot slot, PlugType plugType, LogArgs log)
    {
        Logger.Log(
            new LogArgs(
                LogType.Info,
                this,
                string.Format(
                    Messages.Composer_ErrorOnAutoTaggingExtension,
                    (object) slot.Extension.Id,
                    (object) slot.Extension.Name,
                    (object) -1,
                    (object) plugType.ExtensionType.Name,
                    (object) plugType.Name,
                    (object) log.Message),
                new object[0]));
    }
}
